package cn.webwheel.whtml;

import cn.webwheel.template.FileVisitor;
import cn.webwheel.template.RendererFactory;
import cn.webwheel.template.TemplateRenderer;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.*;
import java.net.URLDecoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class WHandler implements HttpHandler {

    private final File root;

    private String uriEncoding;

    private Map<String, String> mimeMap = new HashMap<String, String>();

    private ObjectMapper mapper = new ObjectMapper();

    private RendererFactory rendererFactory;

    public WHandler(File root, String uriEncoding) throws IOException {
        this.root = root;
        this.uriEncoding = uriEncoding;
        rendererFactory = new RendererFactory(root, null, mapper);
        mapper.getJsonFactory().enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
        mapper.getJsonFactory().enable(JsonParser.Feature.ALLOW_COMMENTS);
        mapper.getJsonFactory().enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
        BufferedReader reader = new BufferedReader(new InputStreamReader(WHandler.class.getResourceAsStream("/res/mime.txt"), "utf-8"));
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                mimeMap.put(line, reader.readLine());
            }
        } finally {
            reader.close();
        }
    }

    private String getWelcome(String path) {
        if (new File(root, path + "index.html").exists()) {
            return path + "index.html";
        }
        return null;
    }

    private void readLine(InputStream is, ByteArrayOutputStream baos) throws IOException {
        baos.reset();
        boolean preisr = false;
        int d;
        while ((d = is.read()) != -1) {
            if (d == '\n' && preisr) {
                return;
            }
            if (preisr) {
                baos.write('\r');
            }
            if (!(preisr = d == '\r')) {
                baos.write(d);
            }
        }
        if (preisr) {
            baos.write('\r');
        }
    }

    private int boundaryEqual(String boundary, ByteArrayOutputStream baos) throws IOException {
        if (boundary.length() + 2 == baos.size()) {
            if (("--" + boundary).equals(new String(baos.toByteArray(), "iso8859-1"))) {
                return 1;
            }
        } else if (boundary.length() + 4 == baos.size()) {
            if (("--" + boundary + "--").equals(new String(baos.toByteArray(), "iso8859-1"))) {
                return 2;
            }
        }
        return 0;
    }

    private Map<String, Object> parse(HttpExchange httpExchange) throws IOException {
        String contentType = httpExchange.getRequestHeaders().getFirst("Content-Type");
        HashMap<String, Object> map = new HashMap<String, Object>();
        if (httpExchange.getRequestMethod().equalsIgnoreCase("post") && contentType != null) {
            if (contentType.equals("application/x-www-form-urlencoded")) {
                int len = Integer.parseInt(httpExchange.getRequestHeaders().getFirst("Content-Length"));
                byte[] buf = new byte[len];
                DataInputStream dis = new DataInputStream(httpExchange.getRequestBody());
                dis.readFully(buf);
                parse(map, new String(buf, "iso8859-1"));
            } else if (contentType.startsWith("multipart/form-data; boundary=")) {
                String boundary = contentType.substring("multipart/form-data; boundary=".length());
                BufferedInputStream is = new BufferedInputStream(httpExchange.getRequestBody());
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                readLine(is, baos);
                int r = boundaryEqual(boundary, baos);
                if (r != 1) return map;
                loop:
                while (true) {
                    String name = null;
                    String filename = null;
                    while (true){
                        readLine(is, baos);
                        if (baos.size() == 0) break;
                        String s = new String(baos.toByteArray(), "iso8859-1");
                        if (s.startsWith("Content-Disposition:")) {
                            for (String ss : s.split(";")) {
                                ss = ss.trim();
                                if (ss.startsWith("name=")) {
                                    name = ss.substring("name=".length() + 1, ss.length() - 1);
                                } else if (ss.startsWith("filename=")) {
                                    filename = ss.substring("filename=".length() + 1, ss.length() - 1);
                                }
                            }
                        }
                    }
                    ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
                    while (true) {
                        readLine(is, baos);
                        r = boundaryEqual(boundary, baos);
                        if (r == 0) {
                            baos2.write(baos.toByteArray());
                            continue;
                        }
                        if (name != null) {
                            if (filename != null) {
                                Object files = map.get(name);
                                if (files == null) {
                                    map.put(name, files = new File[0]);
                                }
                                if (files instanceof File[]) {
                                    File[] fs = (File[]) files;
                                    File[] nfs = new File[fs.length + 1];
                                    System.arraycopy(fs, 0, nfs, 0, fs.length);
                                    nfs[fs.length] = new File(filename);
                                    map.put(name, nfs);
                                }
                            } else {
                                Object vals = map.get(name);
                                if (vals == null) {
                                    map.put(name, vals = new String[0]);
                                }
                                if (vals instanceof String[]) {
                                    String[] vs = (String[]) vals;
                                    String[] nvs = new String[vs.length + 1];
                                    System.arraycopy(vs, 0, nvs, 0, vs.length);
                                    nvs[vs.length] = new String(baos2.toByteArray(), uriEncoding);
                                    map.put(name, nvs);
                                }
                            }
                        }
                        if (r == 1) {
                            continue loop;
                        } else {
                            break loop;
                        }
                    }
                }
            }
        }
        return map;
    }

    private void parse(Map<String, Object> map, String query) throws UnsupportedEncodingException {
        if (query == null) return;
        query = query.trim().replace("&amp;", "&");
        String[] ss = query.split("&");
        for (String s : ss) {
            String[] sss = s.split("=");
            if (sss.length == 2) {
                sss[0] = URLDecoder.decode(sss[0], uriEncoding);
                sss[1] = URLDecoder.decode(sss[1], uriEncoding);
                Object o = map.get(sss[0]);
                if (o == null) {
                    map.put(sss[0], new String[]{sss[1]});
                } else if (o instanceof String[]) {
                    String[] vs = (String[]) o;
                    String[] nvs = new String[vs.length + 1];
                    System.arraycopy(vs, 0, nvs, 0, vs.length);
                    nvs[vs.length] = sss[1];
                    map.put(sss[0], nvs);
                }
            }
        }
    }

    private void send404(HttpExchange httpExchange) throws IOException {
        byte[] msg = "Page NotFound".getBytes("utf-8");
        httpExchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        httpExchange.sendResponseHeaders(404, msg.length);
        httpExchange.getResponseBody().write(msg);
        httpExchange.close();
    }

    private void sendMime(String path, String contentType, String charset, HttpExchange httpExchange) {
        String mime = contentType;
        if (mime == null) {
            int i = path.lastIndexOf('.');
            if (i != -1) {
                mime = mimeMap.get(path.substring(i + 1));
            }
        }
        if (mime == null) return;
        if (mime.startsWith("text") || charset != null) {
            httpExchange.getResponseHeaders().add("Content-Type", mime + "; charset=" + charset);
        } else {
            httpExchange.getResponseHeaders().add("Content-Type", mime);
        }
    }

    private void def(String path, HttpExchange httpExchange) throws IOException {
        File file = new File(root, path);
        if (!file.exists()) {
            send404(httpExchange);
            return;
        }
        byte[] data = new byte[(int) file.length()];
        DataInputStream dis = new DataInputStream(new FileInputStream(file));
        try {
            dis.readFully(data);
        } catch (IOException e) {
            except(httpExchange, e);
            return;
        } finally {
            dis.close();
        }
        sendMime(path, null, null, httpExchange);
        httpExchange.sendResponseHeaders(200, data.length);
        httpExchange.getResponseBody().write(data);
        httpExchange.close();
    }

    private void except(HttpExchange httpExchange, Throwable e) throws IOException {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        except(httpExchange, sw.toString());
    }

    private void except(HttpExchange httpExchange, String e) throws IOException {
        httpExchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        byte[] data = e.getBytes("utf-8");
        httpExchange.sendResponseHeaders(200, data.length);
        httpExchange.getResponseBody().write(data);
        httpExchange.close();
    }

    private void handle(HttpExchange httpExchange, String path, String query, Map<String, Object> params) throws IOException {
        if (path.toLowerCase().endsWith(".html") || path.toLowerCase().endsWith(".htm")) {

            TemplateRenderer renderer;
            StringWriter sw = new StringWriter();
            String rCharset;
            try {
                renderer = rendererFactory.create(path, null, null);
                rCharset = renderer.charset == null ? "utf-8" : renderer.charset;

                StringBuilder sb = new StringBuilder();
                File file = new File(root, path);
                char[] buf = new char[8192];
                int rd;
                InputStreamReader reader = new InputStreamReader(new FileInputStream(file), rCharset);
                try {
                    while ((rd = reader.read(buf)) != -1) {
                        sb.append(buf, 0, rd);
                    }
                } finally {
                    reader.close();
                }
                Map<String, Object> ctx = null;
                Pattern pat = Pattern.compile("<script.*?>\\s*var _model\\s*=\\s*(.+?);?\\s*</script>",
                        Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
                Matcher matcher = pat.matcher(sb.toString());
                if (matcher.find()) {
                    Map<String, Object> map = mapper.readValue(matcher.group(1), Map.class);
                    if (map.size() == 1) {
                        ctx = (Map) map.entrySet().iterator().next().getValue();
                    } else if (map.size() > 1) {
                        String mi = null;
                        try {
                            Object o = params.get("_model");
                            if (o instanceof String[]) {
                                mi = ((String[]) o)[0];
                            }
                        } catch (Exception ignored) {
                        }
                        ctx = (Map) map.get(mi);
                        if (ctx == null) {
                            ModelSelectDlg dlg = new ModelSelectDlg(path, map.keySet());
                            dlg.setVisible(true);
                            if (dlg.selected != null) {
                                ctx = (Map) map.get(dlg.selected);
                            }
                        }
                    }
                }
                if (ctx == null) ctx = Collections.emptyMap();
                renderer.render(sw, ctx);
            } catch (Exception e) {
                except(httpExchange, e);
                return;
            }

            byte[] data = sw.toString().getBytes(rCharset);
            sendMime(path, renderer.contentType, rCharset, httpExchange);
            httpExchange.sendResponseHeaders(200, data.length);
            httpExchange.getResponseBody().write(data);
            httpExchange.close();
            return;
        }
        File file = new File(root, path);
        if (file.exists() && file.length() < 1024 * 1024) {
            Map<String, Object> map = null;
            Reader reader = null;
            try {
                reader = new InputStreamReader(new FileInputStream(file), "utf-8");
                map = mapper.readValue(reader, Map.class);
            } catch (IOException ignored) {
            }
            if (reader != null) reader.close();

            if (map != null) {
                Object obj = null;
                if (map.size() == 1) {
                    obj = map.values().iterator().next();
                } else if (map.size() > 1) {
                    ModelSelectDlg dlg = new ModelSelectDlg(path, map.keySet());
                    dlg.setVisible(true);
                    if (dlg.selected != null) {
                        obj = map.get(dlg.selected);
                    }
                }
                if (obj != null) {
                    byte[] data = null;
                    try {
                        data = mapper.writeValueAsBytes(obj);
                    } catch (IOException ignored) {
                    }
                    if (data != null) {
                        sendMime(".json", null, "utf-8", httpExchange);
                        httpExchange.sendResponseHeaders(200, data.length);
                        httpExchange.getResponseBody().write(data);
                        httpExchange.close();
                        return;
                    }
                }
            }
        }
        def(path, httpExchange);
    }

    private void renderResource(HttpExchange httpExchange, String resource, Map<String, Object> ctx) throws Exception {
        byte[] data;
        if (ctx != null) {
            TemplateRenderer renderer = rendererFactory.create(resource, new FileVisitor() {
                @Override
                public String read(File root, String file, String charset) throws IOException {
                    InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream(file), "utf-8");
                    try {
                        StringBuilder sb = new StringBuilder();
                        int rd;
                        char[] buf = new char[4096];
                        while ((rd = reader.read(buf)) != -1) {
                            sb.append(buf, 0, rd);
                        }
                        return sb.toString();
                    } finally {
                        reader.close();
                    }
                }
            }, null);
            StringWriter sw = new StringWriter();
            try {
                renderer.render(sw, ctx);
            } catch (IOException e) {
                throw e;
            } catch (Exception e) {
                throw new IOException(e);
            }

            data = sw.toString().getBytes("utf-8");
        } else {
            InputStream is = getClass().getResourceAsStream("res/" + resource);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[8192];
            int rd;
            while ((rd = is.read(buf)) != -1) {
                baos.write(buf, 0, rd);
            }
            data = baos.toByteArray();
        }

        sendMime(resource, null, "utf-8", httpExchange);
        httpExchange.sendResponseHeaders(200, data.length);
        httpExchange.getResponseBody().write(data);
        httpExchange.close();
    }

    private void listDir(String path, HttpExchange httpExchange) throws Exception {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("isRoot", path.equals("/"));
        if (!path.equals("/")) {
            map.put("parentPath", path.substring(0, path.lastIndexOf('/', path.length() - 2) + 1));
        }
        File dir = new File(root, path);
        map.put("dir", dir);
        List<File> files = new ArrayList<File>();
        File[] list = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory();
            }
        });
        if (list != null) files.addAll(Arrays.asList(list));
        list = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile();
            }
        });
        if (list != null) files.addAll(Arrays.asList(list));
        map.put("files", files);
        renderResource(httpExchange, "/res/dir.html", map);
    }

    @Override
    public void handle(HttpExchange httpExchange) throws IOException {
        String path = httpExchange.getRequestURI().getPath();
        if (path.endsWith("/")) {
            String welcome = getWelcome(path);
            if (welcome == null) {
                try {
                    listDir(path, httpExchange);
                } catch (Exception e) {
                    except(httpExchange, e);
                }
                return;
            }
            path = welcome;
        }
        String query = httpExchange.getRequestURI().getQuery();
        Map<String, Object> params = parse(httpExchange);
        parse(params, query);
        handle(httpExchange, path, query, params);
    }
}
